<?php if(!defined('SPRITCH')) { exit(); }

// Spritch - An opensource PHP5 web framework

// Author: Luca Degasperi
// Licence: GNU GPL
// Site: http://www.spritch.org/

// Class: ORM
// Summary: This class provides a basic Object Relational Mapper with CRUD functionalities
// Since: 0.9.1

define('RELATION_SINGLE',         'RELATION_SINGLE');
define('RELATION_FOREIGN',        'RELATION_FOREIGN');
define('RELATION_MANY',           'RELATION_MANY');
define('RELATION_NOT_RECOGNIZED', 'RELATION_NOT_RECOGNIZED');
define('RELATION_NOT_ANALYZED',   'RELATION_NOT_ANALYZED');
define('RELATION_CUSTOM',         'RELATION_CUSTOM');

class orm extends spritch
{	
	public static function import_array($class_name,$input) 
	{
		$output = array();
		if($input != false and sizeof($input) > 0)
			foreach ($input as $array) 
			{
				$elm = get_new($class_name.'.model',false);
				$elm->import($array);
				if($elm->id != false)
					$output[] = $elm;
			}
		return sizeof($output) > 0 ? $output : false;	
	}
	
	private $database_info;
	private $database_values;
	private $changed_values;
	private $relations; 
	private $order_property;
	private $order_direction;
	private $db;

	public function settings($table,$fields,$primarykey,$id = false,$connection = array())
	{
		$this->database_info = new stdClass();
		$this->database_info->table = $table;
		$this->database_info->fields = $fields;
		$this->database_info->primary = $primarykey;
		$this->database_info->id = $id;
		if(empty($connection))
			$this->database_info->connection = get('default.database');
		else
			$this->database_info->connection = get($connection['name'].'.database',$connection);
		$this->database_values = array();
		$this->changed_values = array();
		$this->relations = array();
		$this->order_property = false;
		$this->order_direction = false;
		if($id)
			$this->initialize();
	}

	private function initialize() 
	{
		if($this->database_info->id != false)
		{
			$fieldnames = implode(",",array_keys($this->database_info->fields));
			$input = $this->database_info->connection->fetch_row('SELECT '.$fieldnames.' FROM ' .$this->database_info->table.' WHERE '.$this->database_info->primary.' = '.$this->database_info->id,'assoc');
			$this->import($input);
		}
	}

	public function __get($property)
	{ 
		$field = false;
		$field = $this->field_for_property($property);
		if(!$field) $field = (array_key_exists($property, $this->database_info->fields)) ? $property : false;
		if($field != false and array_key_exists($field, $this->changed_values)) return($this->changed_values[$field]);
		if($field != false and is_array($this->database_values) and array_key_exists($field, $this->database_values)) return($this->database_values[$field]);
		if($field === false and is_array($this->database_values) and array_key_exists($property, $this->database_values)) return($this->database_values[$property]);
		if($field === false)
		{
			get('profiler')->warning('Field not found in '.get_class($this).' property: '.$property);
			return false;
		}
	}

	public function __set($property,$value)
	{
		if($this->has_property($property))
			$this->changed_values[$this->field_for_property($property)] = $value;	
		else
			get('profiler')->warning('Tried to set a non-orm property for '.get_class($this).' property: '.$property.' value: '.$value);	
	}	

	public function __sleep() 
	{
		return array_keys(get_object_vars($this));
	}

	public function has_property($property)
	{ 
		if (array_key_exists($property,$this->database_info->fields) !== false or array_search($property,$this->database_info->fields) !== false or array_search($property,$this->database_values) !== false)
			return true;
		return false;
	}

	public function field_for_property($property)
	{
		if(array_key_exists($property,$this->database_info->fields))
			return($property);
		if(array_search($property, $this->database_info->fields) !== false)
			return(array_search($property, $this->database_info->fields));		
		return false;
	}

	public function delete_yourself()
	{
		if($this->database_info->id !== false)
			$this->database_info->connection->query('DELETE FROM '.$this->database_info->table.' WHERE '.$this->database_info->primary.' = '.$this->database_info->id);
	}

	public function set_order_property($field,$order = 'ASC')
	{ 
		$this->order_property = $field;
		$this->order_direction = $order;
	}
	
	private function insert_new()
	{
		if($this->id == false)
		{
			$insert_fields = $this->database_info->primary;
			$insert_values = 'null';
		}
		if (sizeof($this->changed_values) > 0)
		{
			$filtered_values = $this->changed_values;
			$insert_fields .= ', '.implode(",", array_keys($filtered_values));
			foreach ($filtered_values as $property=>$value)
			{
				$insert_values .= ", '".$this->database_info->connection->escape_value($value)."'";
				$this->database_values[$property] = $this->changed_values[$property];
			}
			$this->changed_values = array();
		}		
		$this->database_info->id = $this->database_info->connection->query('INSERT INTO '.$this->database_info->table.' ('.$insert_fields.') VALUES ('.$insert_values.')');
		$this->database_values[$this->database_info->primary] = $this->database_info->id;
		return $this->database_info->id;
	}

	public function save() 
	{
		if(sizeof($this->changed_values) > 0 and $this->database_info->id == false)
		{
			$id = $this->insert_new();
			$this->analyze_relations();		
			return $id;
		}
		else if ($this->changed_values != false)
		{
			$update_query = '';
			$filtered_values = $this->changed_values;
			foreach($filtered_values as $property=>$value)
			{
				$update_query .= ($update_query != '') ? ', ' : '';
				$update_query .= ($value != '') ? " {$property} = '".$this->database_info->connection->escape_value($value)."'" : "{$property} = NULL";
				$this->database_values[$property] = $this->changed_values[$property];
			}
			$this->database_info->connection->query("update ".$this->database_info->table." set ".$update_query." where ".$this->database_info->primary." = ".$this->database_info->id);
			$this->changed_values = array(); 
			return $this->database_info->id;
		}	
		return false;
	}

	public function add_relation($class_name,$connector_class_name=false) 
	{
		$this->relations[$class_name] = new stdClass();
		$this->relations[$class_name]->relation_type = RELATION_NOT_ANALYZED;
		if($connector_class_name != false)
			$this->relations[$class_name]->connector_class = $connector_class_name;
		if($this->database_info->id != false)
			$this->analyze_relations();		
	}

	function add_custom_relation($class_name, $source_property, $target_property)
	{
		if(!$this->has_property($source_property))
			get('profiler')->warning('Error in add_custom_relation: '.get_class($this).' hasnt got property '.$source_property.', so couldnt connect to '.$class_name);
		else
		{
			$this->relations[$class_name] = new stdClass();
			$this->relations[$class_name]->relation_type = RELATION_CUSTOM;					
			$this->relations[$class_name]->source_property = $source_property;
			$this->relations[$class_name]->target_property = $target_property;
		}
	}

	public function analyze_relations() 
	{
		foreach($this->relations as $class_name => $info)
		{
			if(is_subclass_of($class_name,'orm'))
			{
				$obj = get_new($class_name.'.model',false);
				$info->class_name = $class_name;
				if($info->relation_type == RELATION_NOT_ANALYZED)
				{
					if(array_key_exists('connector_class',get_object_vars($info)) and $info->connector_class != '' and is_subclass_of($info->connector_class,'orm'))
					{
						$connector = $info->connector_class;
						$connector_obj = get_new($connector.'.model',false);
						if(array_key_exists($this->database_info->primary, $connector_obj->database_info->fields) and array_key_exists($obj->database_info->primary, $connector_obj->database_info->fields))
							$info->relation_type = RELATION_MANY; 
						else 
							unset($info->connector_class);
					}
					if(array_key_exists($obj->database_info->primary, $this->database_info->fields) and array_key_exists($this->database_info->primary, $obj->database_info->fields))
						$info->relation_type = RELATION_SINGLE;
					else if((array_key_exists($this->database_info->primary, $obj->database_info->fields) and !array_key_exists($obj->database_info->primary, $this->database_info->fields) or !array_key_exists($this->database_info->primary, $obj->database_info->fields) and array_key_exists($obj->database_info->primary, $this->database_info->fields)))
						$info->relation_type = RELATION_FOREIGN;		
					else if($info->relation_type == RELATION_NOT_ANALYZED)
					{
						$info->relation_type = RELATION_NOT_RECOGNIZED;
						get('profiler')->warning('Relation not recognized! '.$class_name.' connecting to '.get_class($this)); 
					}
					$this->relations[$class_name] = $info;
				}
			}
			else
			{
				get('profiler')->warning($class_name.' is not a orm!');
				unset($this->relations[$class_name]);
			}
		}	
	}

	public function connect($object) 
	{
		$class_name = get_class($object);
		if($this->database_info->id == false) $this->save();
		if($object instanceof orm and $object->database_info->id == false) $object->save(); 	
		if(array_key_exists($class_name, $this->relations))
		{
			switch($this->relations[$class_name]->relation_type)
			{
				case RELATION_NOT_ANALYZED:
					$this->analyze_relations();
					$this->connect($object);
				break;
				case RELATION_SINGLE:
					$this->changed_values[$object->database_info->primary] = $object->database_info->id;
					$object->changed_values[$this->database_info->primary] = $this->database_info->id;	 
				break;
				case RELATION_FOREIGN:
					if(array_key_exists($this->database_info->primary, $object->database_info->fields))
						$object->changed_values[$this->database_info->primary] = $this->database_info->id;
					else if(array_key_exists($object->database_info->primary, $this->database_info->fields))
						$this->changed_values[$object->database_info->primary] = $object->database_info->id;
				break;
				case RELATION_MANY:
					$connector = $this->relations[$class_name]->connector_class;
					$connector = new $connector(false);
					$property = $connector->database_info->fields[$this->database_info->primary];
					$connector->$property = $this->database_info->id;
					$property = $connector->database_info->fields[$object->database_info->primary];
					$connector->$property = $object->database_info->id;
					$connector->save();
				break;
				case RELATION_CUSTOM: 
					if($this->field_for_property($this->relations[$class_name]->source_property) != $this->database_info->primary)
					{
						$targetval = $this->relations[$class_name]->target_property;
						$this->changed_values[$this->field_for_property($this->relations[$class_name]->source_property)] = $object->$targetval;
					}
					else
					{
						$targetval = $this->relations[$class_name]->source_property;
						$object->changed_values[$this->relations[$class_name]->target_property] = $this->id;	
					}
				break;
			}
			$this->save();
			$object->save();
		}	
	}

	public function disconnect($object,$id = false) 
	{
		if(!$object and !$id) return;
		if(!$object instanceof orm and $id != false)
		{
			$object = get_new($object.'.model',false);
			$object->database_info->id = $id;
		}
		$class_name = get_class($object);
		if(array_key_exists($class_name, $this->relations))
		{
			switch($this->relations[$class_name]->relation_type)
			{
				case RELATION_SINGLE:
					$this->changed_values[$object->database_info->primary] = '';
					$object->changed_values[$this->database_info->primary] = '';
				break;
				case RELATION_FOREIGN:
					if(array_key_exists($this->database_info->primary, $object->database_info->fields))
						$object->changed_values[$this->database_info->primary] = '';
					else if(array_key_exists($object->database_info->primary, $this->database_info->fields))
						$this->changed_values[$object->database_info->primary] = '';
				break;
				case RELATION_MANY:
					$input = orm::search($this->relations[$class_name]->connector_class,array($object->database_info->primary => $object->database_info->id, $this->database_info->primary => $this->database_info->id));
					if($input)
						$input[0]->delete_your_self();
				break;
				case RELATION_CUSTOM: 
					if($this->field_for_property($this->relations[$class_name]->source_property) != $this->database_info->primary)
					{
						$targetval = $this->relations[$class_name]->target_property;
						$this->changed_values[$this->field_for_property($this->relations[$class_name]->source_property)] = '';
					}
					else
					{
						$targetval = $this->relations[$class_name]->source_property;
						$object->changed_values[$this->relations[$class_name]->target_property] = '';	
					}
				break;
			}
			$this->save();
			$object->save();
		}	
	}

	private function is_connector($class_name)
	{
		foreach ($this->relations as $key => $val)
			if(array_key_exists('connector_class', get_object_vars($val)) and $val->connector_class == $class_name)
				return true; 
		return false;	
	}

	public function import($values)
	{ 
		$this->database_values = $values;
		$this->database_info->id = (!empty($values[$this->database_info->primary])) ? $values[$this->database_info->primary] : false;
	}

	public function import_defaults($array)
	{
		foreach($array as $key=>$val) 
			$this->$key = $val;
	}

	private function is_relation($class) 
	{
		if (strtolower($class) == strtolower(get_class($this)))
			return(get_class($this));
		if(!empty($this->relations))
			foreach($this->relations as $key=>$val)
				if(strtolower($class) == strtolower($key))
					return($key);
		get('profiler')->warning('Error in is_relation! '.$class.' is not a relation of '.get_class($this));
		return false;
	}
}
?>